<?
/**
* @author Дубров Олег
* 
* Абстрактный класс записи в таблице
*/

class Core_Record implements ArrayAccess, Iterator{
	const
		INSERT_FLAG = 'insert',
		UPDATE_FLAG = 'update',
		REPLACE_FLAG = 'replace';
	
  	protected 
		$_db_name,
		$_table_name,
 		$_sql_create,
        $_primary_key_field = 'id',
		$save_type = 'insert',
    	$errors,
    	$errorsByForm;

	private static 
		$_schema,
    	$_cache = array(),
    	$_initialized = array(),
    	$_checkers = array();	
    	
	private 
		$_types,
		$_data = array(),
        $_data_modified,
		$_sql_buffer,
        $_next = null, // Следующий объект (актуально только для списка объектов)
        $_prev = null; // Предыдущий объект (актуально только для списка объектов)

	
	public function __construct($id = false, $table_name = false, $primary_key = false)
	{
		if($table_name) list($this->_table_name, $this->_db_name) = array_reverse(explode('.', $table_name));
        if($primary_key) $this->_primary_key_field = $primary_key;
		
		if (!isset(self::$_initialized[get_class($this)])) {
			$this->_init(); //Вызовется только в случае, если для такого типа объектов еще не заданы checker'ы
			self::$_initialized[get_class($this)] = true;
		}
		
		if($id === false) return;
		
		$this->load($id);
	}
	
	protected function _init() {}
    
    public function getId(){ return $this[$this->_primary_key_field]; }
    public function getClass(){ return get_class($this); }
    public function getNext(){ 
        $class_name = $this->getClass();
        return new $class_name($this->_next); 
    }
    public function getPrev(){ 
        $class_name = $this->getClass();
        return new $class_name($this->_prev); 
    }
    
    public function hasNext(){ return (boolean) $this->_next; }
    public function hasPrev(){ return (boolean) $this->_prev; }
    
    
    public function setNext($value){ $this->_next = $value; }
    public function setPrev($value){ $this->_prev = $value; }
	
	public function replaceSaveType($is_replace)
	{
		$this->save_type = ($is_replace) ? 'replace' : 'insert';
	}
	
	public static function clearCache()
	{
		self::$_cache = array();
	}
	
	public function parseCreateSql(){
        // not implemented
        return false;
	}
    
    static public function getTablePrimaryKey($table_name){
        $key_fields = self::getTablePrimaryKeys($table_name);
        if(empty($key_fields)) return false;
        if(count($key_fields) > 1) return false;
        return array_pop($key_fields);
    }
    
    
    static public function getTablePrimaryKeys($table_name){
        $fields = Database::select("SHOW COLUMNS FROM $table_name", true);
        $key_fields = array();
        foreach($fields as $one)
            if($one['Key'] == 'PRI') $key_fields[] = $one['Field'];
        if(empty($key_fields)) return false;
        return $key_fields;
    }
    
    static public function getTableFirstField($table_name){
        $fields = Database::select("SHOW COLUMNS FROM $table_name", true);
        if(!count($fields)) return false;
        return $fields[0]['Field'];
    }
    
    static public function getTableFields($table_name){
        $fields = Database::select("SHOW COLUMNS FROM $table_name", true);
        $ret = array();
        foreach($fields as $one) $ret[] = $one['Field'];
        return $ret;
    }

	public function tableExists(){
		Database::query("show tables from ".$this->_db_name);
		$tables = Database::fetchAll();
		foreach($tables as $one)
			if(reset($one) == $this->getTableName()) return true;
		return false;
	}
	
	public function __set($varname, $value){ $this[$varname] = $value; }
	public function __get($varname){ return $this[$varname]; }
	
	public function getDataModified(){
		return $this->_data_modified;
	}
	
	public function isModified($field_name){
		return isset($this->_data_modified[$field_name]);
	}
	
	public function getModifiedArray()
	{
		return $this->_data_modified;
	}
	
    /** Установка данных объекта и проверка на изменение */
	public function loadFromArray($arr, $mark_as_modified = true){
        if(!is_array($arr)) return;
        if($mark_as_modified)
            foreach($arr as $key=>$val)
                if(!isset($this->_data[$key]) || $this->_data[$key] != $val)
                    $this->_data_modified[$key] = $val;
		$this->_data = array_merge($this->_data, $arr);
	}
    
    /** Установка данных объекта без проверки на изменение */
    public function getFromArray($arr){
        $this->setData($arr);
        // Сохраняем в кэш
        self::$_cache[get_class($this).$this->getId()] = $this;
        $this->afterLoad();
    }
	
	/** Установка данных объекта без проверки на изменение */
    public function setData($arr){
		$this->_data = $arr;
	}	

	public function load($id, $force_load = false){
        // Если этот объект уже загружен, загружаем из кэша
        if(!$force_load && !empty(self::$_cache[get_class($this).$id])){
            $this->_data = self::$_cache[get_class($this).$id]->_data;
            $this->_next = self::$_cache[get_class($this).$id]->_next;
            $this->_prev = self::$_cache[get_class($this).$id]->_prev;
            return true;
        }
        // Сохраняем в кэш
        self::$_cache[get_class($this).$id] = $this;
		return $this->loadByWhere("`{$this->_primary_key_field}`='$id'");
	}
	
	public function loadByWhere($where){
		$sql = "select * from {$this->getFullTableName()} where $where order by `{$this->_primary_key_field}` desc limit 1";
		Database::query($sql);
		$row = Database::FetchRow();
		if(!$row) return false;//throw new Exception("Не найден объект типа {$this->getTableName()} id='$id'");
		$this->getFromArray($row);
        return true;
	}
	
	public function getTableName(){
		return $this->_table_name;
	}

    public function setTableName($value){
        $this->_table_name = $value;
    }

	public function getFullTableName(){
		if($this->_db_name)
			return "`{$this->_db_name}`.`{$this->getTableName()}`";
		else
			return "`{$this->getTableName()}`";
	}

    public function getDatabaseName(){
        return $this->_db_name;
    }
    
    /**
    * @param CSQLBuffer  $sql_buffer
    */
    public function setSQLBuffer($sql_buffer){
        $this->_sql_buffer = $sql_buffer;
    }
    
    public function insert($ignore = false){
        if($this->_sql_buffer){
            $this->_sql_buffer->setTable($this->getFullTableName());
            $this->_sql_buffer->insertData($this->_data);
            return;
        }    
	
		if ($this->beforeWrite(self::INSERT_FLAG) === false) return false;
		
		$sql = "insert ".($ignore ? 'ignore ' : '')."into {$this->getFullTableName()} ";
		$base_fields = $this->getTableFields($this->getFullTableName());
				
		$values = array_intersect_key($this->_data, array_flip($base_fields));
		$pares = array();
        if(!empty($values)) {
		    foreach($values as $key=>$val) {
				if (!is_null($val))
                	$pares[] = "`$key`='".mysql_real_escape_string($val)."'"; 
			}
		}
        else {
        	$pares[] = "`{$this->_primary_key_field}`=''";
		}
		$sql.='set '.join(', ', $pares);

		Database::query($sql);
    	$this[$this->_primary_key_field] = Database::getInsertID();

    	$this->afterWrite(self::INSERT_FLAG);
    	
    	return Database::GetAffectedRows();
	}

    
    public function restore($commit_id = false){
        $sql = "insert ignore into {$this->getFullTableName()} ";
        $base_fields = $this->getTableFields($this->getFullTableName());
                
        $values = array_intersect_key($this->_data, array_flip($base_fields));
        $pares = array();
        if(!empty($values)) {
            foreach($values as $key=>$val) {
            	if (is_null($val)) {
            		$pares[] = "`$key`= NULL"; 
				} else {
                	$pares[] = "`$key`='".mysql_real_escape_string($val)."'"; 
				}
			}
        }
        else {
            $pares[] = "`{$this->_primary_key_field}`=''";
        }
        $sql.='set '.join(', ', $pares);
        
        Database::query($sql);
        $this[$this->_primary_key_field] = Database::getInsertID();
        
        return Database::GetAffectedRows();
    }
    
	public function replace(){
		
		if ($this->beforeWrite(self::REPLACE_FLAG) === false) return false;
		
		$sql = "replace into {$this->getFullTableName()} ";
		
		$base_fields = $this->getTableFields($this->getFullTableName());
		$values = array_intersect_key($this->_data, array_flip($base_fields));
				
		$pares = array();
		foreach($values as $key=>$val) {
			if (!is_null($val)) {
                $pares[] = "`$key`='".mysql_real_escape_string($val)."'"; 
			}
		}
		
		$sql.='set '.join(', ', $pares);
		Database::query($sql);
    	$this[$this->_primary_key_field] = Database::getInsertID();
    	
    	$this->afterWrite(self::REPLACE_FLAG);
    	
    	return Database::GetAffectedRows();
	}
	
	public function update(){
		
		if ($this->beforeWrite(self::UPDATE_FLAG) === false) return false;
		
		$sql = "update {$this->getFullTableName()} ";
		
		$pares = array();		
		$base_fields = $this->getTableFields($this->getFullTableName());
		if (!empty($this->_data_modified)) {
			$values = array_intersect_key($this->_data_modified, array_flip($base_fields));
		}
		if(empty($values)) return;
		foreach($values as $key=>$val) {
			if (is_null($val)) {
            		$pares[] = "`$key`= NULL"; 
				} else {
                	$pares[] = "`$key`='".mysql_real_escape_string($val)."'"; 
				}			
		}
			
		$sql.='set '.join(', ', $pares)." where `{$this->_primary_key_field}` = '{$this[$this->_primary_key_field]}'";
		Database::query($sql);
		$result = Database::GetAffectedRows();
		$this->afterWrite(self::UPDATE_FLAG);
    	return $result;
	}	
	
	public function delete(){
		$sql = "delete from {$this->getFullTableName()} where `{$this->_primary_key_field}` = '{$this[$this->_primary_key_field]}' limit 1";
		Database::query($sql);
	}
    
    public function asArray(){
    	return $this->_data;
	}	
	
	function getPrimaryKeyField()
	{
		return $this->_primary_key_field;
	}
	
	/**
	* Возвращает объект визуализации свойств
	* @return Core_RecordView
	*/
	public function view($hide_error = false)
	{
		return new Core_RecordView($this, $hide_error);
	}
	
	
	/**
    * Возвращает подготовленное для отображения значение свойства
    * 
    * @param string $property
    */
	public function textView($property = null)
	{
		$text_view = new Core_RecordTextView($this);
		if ($property !== null) return $text_view->text($property); //Для совместимости
		return $text_view;
	}	
	
	/**
	* Добавляет Чекер к полю core_record'а.
	* 
	* @param string $property
	* @param callback $checkFunc
	* @param string $errorText
	*/
	public function setChecker($property, $checkFunc, $errorText)
	{
		self::$_checkers[get_class($this)][$property][] = array(
			'callback' => $checkFunc,
			'params' => array_slice(func_get_args(), 2)
		);
	}
	
	/**
	* Получить Все Чекеры
	*/
	public function getCheckers()
	{
		return isset(self::$_checkers[get_class($this)]) ? self::$_checkers[get_class($this)] : array();
	}
	
	/**
	* Удаляет все Чекеры у поля
	* 
	* @param string $property
	*/
	public function removeCheckers($property)
	{
		unset(self::$_checkers[get_class($this)][$property]);
	}
	
	/**
	* Сохраняет объект в случае успешной проверки значений свойств
	* Возвращает true в случае успеха, иначе - false
	* 
	* @param mixed $primaryKey
	* @return bool
	*/
	public function save($postVar = null, $exceptKeys = array(), $replace = false)
	{
		if (!$this->check($postVar, $exceptKeys)) return false;
		
		//Выполняем сохранение объекта
		if ($this->getId()) {
			if ($replace) 
				$this->replace(); 
			else 
				$this->update();
		} else {
			$func = $this->save_type;
			$this->$func();
		}
		return true;
	}
	
	/**
	* Заполнит объект из $postVar. Проверит значения на ошибки
	* 
	* @param array $postVar
	* @return bool. true - если ошибок нет, иначе false
	*/
	public function check($postVar = null, $exceptKeys = null)
	{
		if ($postVar !== null && $exceptKeys !== null) {
			$postVar = array_diff_key($postVar, array_flip($exceptKeys));
		}
		if ($postVar !== null) $this->loadFromArray($postVar);
		$checkers = $this->getCheckers();
		$this->cleanErrors();
		
		$CHK = new Core_Checker();
		$return = true;
		
		//$base_fields = $this->getTableFields($this->getFullTableName());
		
		foreach(array_keys($checkers) as $property) {
			if ($exceptKeys && in_array($property, $exceptKeys)) continue; //Не проверяем исключенные поля.
			
			$value = isset($this->_data[$property]) ? $this->_data[$property] : null;

			foreach($checkers[$property] as $checker) {
				if (is_string($checker['callback'])) {
					$callback = array($CHK, $checker['callback']);
				} else {
					$callback = $checker['callback'];
				}
				
				$params = array_merge(array($this, $this[$property]), $checker['params']);
				$result = call_user_func_array($callback, $params);
				if ($result !== true) {
					$this->addError($result, $property);
					$return = false;
				}
			}
		}
		return $return;
	}
	
	/**
	* Очищает объект от значений
	*/
	function clear()
	{
		$this->_data = array();
		$this->_data_modified = null;		
	}
	
	/**
	* Очищает пометку о том что поле модифицировано
	* 
	* @param string|null $field_name
	*/
	function cleanModifyMark($field_name = null)
	{
		if ($field_name === null) {
			$this->_data_modified = null;
		} else {
			unset($this->_data_modified[$field_name]);
		}
	}
	
	/********* События **********************************/
	
	
	
	/**
	* Вызывается после вызова метода getFromArray()
	*/
	public function afterLoad()
	{
		
	}
	
	/**
	* Вызывается перед сохранением объекта
	*/
	public function beforeWrite()
	{
		
	}
	
	/**
	* Вызывается после сохранения объекта
	*/
	public function afterWrite()
	{
		
	}
	
	/********* Раздел управления ошибками **************/
	
	public function addError($errorText, $property = null)
	{
		$this->errors[] = $errorText;
		if ($property !== null) $this->errorsByForm[$property][] = $errorText;
	}
	
	public function getErrorsByForm($property = null)
	{
		if (!$property) return $this->errorsByForm;
		return isset($this->errorsByForm[$property]) ? $this->errorsByForm[$property] : array();
	}
	
	public function getErrorsInline($property)
	{
		return implode(',', $this->getErrorsByForm($property));
	}
	
	public function getErrors()
	{
		return $this->errors;
	}
	
	public function isErrors($property = null)
	{
		if ($property === null) return !empty($this->errors);
		else return !empty($this->errorsByForm[$property]);
	}
	
	public function cleanErrors()
	{
		$this->errors = array();
		$this->errorsByForm = array();
	}
	
	/**
	* Переносит ошибки формы из другого объекта
	* 
	* @param Core_Record $other_object
	* @param string $src_field  имя свойства другого объекта
	* @param string $dst_field имя свойства текущего объекта
	*/
	public function importFormErrors($other_object, $src_field, $dst_field)
	{
		$errors = $other_object->getErrorsByForm($src_field);
		foreach($errors as $error) {
			$this->addError($error, $dst_field);
		}
	}
	
	/********* Конец раздела управления ошибками *********/
	
    
    public function current (){
        return current($this->_data);
    }
    public function key (){
        return key($this->_data);
    }
    public function next (){
        return next($this->_data);
    }
    public function rewind (){
        return reset($this->_data);
    }
    public function valid (){
        return key($this->_data) !== null;
    }

	
	public function offsetExists($offset){
		return isset($this->_data[$offset]);
	}
	
	public function offsetGet($offset){
        if(!isset($this->_data[$offset])) return null;
		return $this->_data[$offset];
	}
	
	public function offsetSet($offset, $val){
		$this->_data[$offset] = $val;
		$this->_data_modified[$offset] = $val;
	}
	
	public function offsetUnset($offset){
		unset($this->_data[$offset]);
	}
    
    public function getGlobalId(){
        return self::generateGlobalId($this->getClass(), $this->getId());
    }

    static public function generateGlobalId($class_name, $id){
        $result = '';
        $b64 = str_split($class_name.'|'.$id);
        foreach($b64 as $one) $result .= dechex(ord($one)-1);
        return $result;
    }
    
    static public function globalIdToString($global_id){
        $result = "";
        foreach(str_split($global_id, 2) as $one) $result .= chr(hexdec($one)+1);
        return $result;
    }

    static public function globalIdToArray($global_id){
        $parts = explode('|', self::globalIdToString($global_id));
        if(count($parts) != 2) return array("","");
        return $parts;
    }
    
    static public function createFromGlobalId($global_id){
        list($class_name, $id) = self::globalIdToArray($global_id);
        if(!class_exists($class_name)) return;
        return new $class_name((int)$id);
    }
    
}
?>
